package md.sv.dbu.executor;

import md.sv.dbu.DBUExecutor;
import md.sv.dbu.config.XmlParser;
import md.sv.dbu.config.jaxb.*;
import md.sv.dbu.db.DBExecutor;
import md.sv.dbu.receiver.MessageReceiver;
import md.sv.dbu.utils.DbuUtils;
import md.sv.dbu.utils.PatternReplacer;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.w3c.dom.Document;

import java.util.*;

/**
 * @author MG
 */
public abstract class ReceiveExecutor<T> implements DbuTypeExecutor {
    private static final Logger logger = LoggerFactory.getLogger(ReceiveExecutor.class);

    protected static class ReceiveTask {
        protected ReceiveTaskType receiveTaskType;
        protected SqlConfigType sqlConfig;
        protected String[] validateFiles;
        protected String templateFile;
        protected String resultSuccessTemplateFile;
    }

    protected DBUExecutor dbuExecutor;
    protected MessageReceiver<T> messageReceiver;
    private Class<T> acceptClass;


    public ReceiveExecutor(MessageReceiver<T> messageReceiver, Class<T> acceptClass) {
        this.messageReceiver = messageReceiver;
        this.acceptClass = acceptClass;
    }

    @Override
    public void registerMainExecutor(DBUExecutor executor) {
        this.dbuExecutor = executor;
    }

    @Override
    public boolean isEligible(Object dbuElement) {
        return acceptClass.isInstance(dbuElement);
    }

    @Override
    public void execute(Object dbuElement) {
        T typeElement = acceptClass.cast(dbuElement);
        executeType(typeElement);
    }

    public abstract void executeType(T typeElement);

    public abstract void onMessageNotProcessed(String mqMessage);


    protected boolean executeReceiveTasks(String mqMessage, List<ReceiveTask> receiveTasks) {
        final Map<String, List<Object>> templateXmlValues = new HashMap<String, List<Object>>();

        for (ReceiveTask receiveTask : receiveTasks) {
            if (!receiveTask.receiveTaskType.isExecute()) {
                continue;
            }

            if (!dbuExecutor.getXmlParser().validateXml(mqMessage, receiveTask.validateFiles)) {
                logger.debug("mqMessage not validated against XSD for Receive Task [{}]. Skipping...", receiveTask.receiveTaskType.getDescription());
                continue;
            }

            logger.info("Executing receive task: [{}]", receiveTask.receiveTaskType.getDescription());

            Document xmlDoc = DbuUtils.getDocumentFromString(mqMessage);
            if (xmlDoc == null) {
                logger.error("Could not parse MQ message");
                continue;
            }

            final Map<String, String> templates = new HashMap<String, String>();

            dbuExecutor.getXmlParser().parseXmlDocument(DbuUtils.getDocumentFromString(receiveTask.templateFile), new XmlParser.XmlParserCallback() {
                @Override
                public void setXmlValue(String nodeFullName, String value) {
                    String varName = dbuExecutor.getSqlVariablesReplacer().getVariableName(value);
                    if (varName == null) {
                        varName = dbuExecutor.getGlobalVariablesReplacer().getVariableName(value);
                    }
                    templates.put(nodeFullName, varName);
                }
            });

            final Map<String, List<Integer>> xmlValuesOrder = new HashMap<String, List<Integer>>();
            dbuExecutor.getXmlParser().parseXmlDocument(xmlDoc, new XmlParser.XmlParserCallback() {
                private int orderIndex = 1;

                @Override
                public void setXmlValue(String nodeFullName, String value) {
                    String varName = templates.get(nodeFullName);
                    if (varName == null) {
                        return;
                    }
                    // insert value
                    List<Object> valuesList = templateXmlValues.get(varName);
                    if (valuesList == null) {
                        valuesList = new ArrayList<Object>();
                        templateXmlValues.put(varName, valuesList);
                    }
                    valuesList.add(PatternReplacer.parseVariable(varName, value, dbuExecutor.getDateTimeFormats()));

                    //insert order index
                    List<Integer> insertOrder = xmlValuesOrder.get(varName);
                    if (insertOrder == null) {
                        insertOrder = new ArrayList<Integer>();
                        xmlValuesOrder.put(varName, insertOrder);
                    }
                    insertOrder.add(orderIndex++);
                }
            });

            final Map<String, List<Object>> xmlValues = new HashMap<String, List<Object>>();
            // reduce any compound variable names to simple names
            for (String varName : templateXmlValues.keySet()) {
                String simpleVarName = PatternReplacer.getVariableSimpleName(varName);
                xmlValues.put(simpleVarName, templateXmlValues.get(varName));
            }

            if (receiveTask.receiveTaskType.getFileLogger().isEnabled()) {
                StringBuilder logFileName = new StringBuilder(receiveTask.receiveTaskType.getFileLogger().getPatternFileName());
                dbuExecutor.getGlobalVariablesReplacer().replaceVariables(logFileName, dbuExecutor.getVariables(), dbuExecutor.getDateTimeFormats());
                dbuExecutor.getSqlVariablesReplacer().replaceVariables(logFileName, xmlValues, dbuExecutor.getDateTimeFormats());
                String logFilePath = FilenameUtils.concat(FilenameUtils.concat(dbuExecutor.getRootPath(), receiveTask.receiveTaskType.getFileLogger().getPath()), logFileName.toString());
                logger.debug("Saving XML file to [{}]", logFilePath);
                DbuUtils.writeFile(logFilePath, mqMessage);

                String variableValue = FilenameUtils.concat(receiveTask.receiveTaskType.getFileLogger().getPath(), logFileName.toString());
                dbuExecutor.getVariables().put(receiveTask.receiveTaskType.getFileLogger().getVariableName(), variableValue);
            }

            DBExecutor executor = dbuExecutor.getDbExecutors().get(receiveTask.receiveTaskType.getDatabase());
            if (executor == null) {
                logger.error("Database connection is not provided or is incorrect for the receive task: {}", receiveTask.receiveTaskType.getDescription());
                onMessageNotProcessed(mqMessage);
                return false;
            }

            // add results from select queries to values list
            for (SelectSQLType selectSql : receiveTask.sqlConfig.getSelectSQL()) {
                Set<String> sqlParameters = dbuExecutor.getSqlParamsReplacer().getVariablesSet(selectSql.getValue());
                MapSqlParameterSource parameterSource = new MapSqlParameterSource();
                for (String parameter : sqlParameters) {
                    List<Object> values = xmlValues.get(parameter);
                    if (values != null && !values.isEmpty()) {
                        parameterSource.addValue(parameter, values.get(0));
                    }
                }
                DBExecutor selectExecutor = dbuExecutor.getExecutor(executor, selectSql.getDatabase());
                if (selectExecutor == null) {
                    logger.warn("DB executor {} defined for select query is invalid in receive  task: {}", selectSql.getDatabase(), receiveTask.receiveTaskType.getDescription());
                    continue;
                }
                List<MapSqlParameterSource> valuesParams = selectExecutor.selectQuery(selectSql.getValue(), parameterSource, dbuExecutor.getVariables());
                for (final MapSqlParameterSource source : valuesParams) {
                    for (final String name : source.getValues().keySet()) {
                        if (dbuExecutor.getVariables().get(name) == null) {
                            List<Object> values = xmlValues.get(name);
                            if (values == null) {
                                values = new ArrayList<Object>();
                                xmlValues.put(name, values);
                            }
                            values.add(source.getValue(name));
                        } else {
                            xmlValues.put(name, new ArrayList<Object>(1){{add(source.getValue(name));}});
                        }
                    }
                }
            }

            if (receiveTask.receiveTaskType.getDbLogger().isEnabled()) {
                boolean result = executeUpdateBlocks(receiveTask.sqlConfig.getUpdateBlock(), xmlValues, xmlValuesOrder, executor);
                if (!result) {
                    onMessageNotProcessed(mqMessage);
                    return false;
                }

                for (SwitchType switchType : receiveTask.sqlConfig.getSwitch()) {
                    List<Object> values = xmlValues.get(switchType.getVariable());
                    if (values == null || values.size() == 0) {
                        logger.warn("Variable {} specified for switch, but it has no values", switchType.getVariable());
                        continue;
                    }
                    String checkValue = values.iterator().next().toString();
                    boolean caseExecuted = false;
                    for (CaseType caseType : switchType.getCase()) {
                        if (caseType.getValue().equals(checkValue)) {
                            executeUpdateBlocks(caseType.getUpdateBlock(), xmlValues, xmlValuesOrder, executor);
                            caseExecuted = true;
                            break;
                        }
                    }
                    if (!caseExecuted) {
                        executeUpdateBlocks(switchType.getDefault().getUpdateBlock(), xmlValues, xmlValuesOrder, executor);
                    }
                }
            }

            //send result success xml if enabled
            sendMQResult(receiveTask.resultSuccessTemplateFile, xmlValues);
            templateXmlValues.clear();
            return true;
        }
        onMessageNotProcessed(mqMessage);
        return false;
    }

    private boolean executeUpdateBlocks(List<UpdateBlockType> updateBlocks, Map<String, List<Object>> xmlValues, Map<String, List<Integer>> xmlValuesOrder, DBExecutor executor) {
        for (UpdateBlockType updateBlock : updateBlocks) {
            String dbSql = DBExecutor.getSqlFromUpdateBlock(updateBlock);
            Set<String> sqlParameters = dbuExecutor.getSqlParamsReplacer().getVariablesSet(dbSql);

            // if this is an insert, and the required check value is specified, but has no values, then no inserts will be done
            if (updateBlock.getInsertSQL() != null && updateBlock.getInsertSQL().getRequiredCheckValue() != null &&
                    xmlValues.get(updateBlock.getInsertSQL().getRequiredCheckValue()) == null) {
                logger.debug("Skipping insert for table [{}] because the required check value [{}] has no values",
                        updateBlock.getInsertSQL().getTable(), updateBlock.getInsertSQL().getRequiredCheckValue());
                continue;
            }

            // if this is an execute, and the required check value is specified, but has no values, then no inserts will be done
            if (updateBlock.getExecuteSQL() != null && updateBlock.getExecuteSQL().getRequiredCheckValue() != null &&
                    xmlValues.get(updateBlock.getExecuteSQL().getRequiredCheckValue()) == null) {
                logger.debug("Skipping execute [{}] because the required check value [{}] has no values",
                        updateBlock.getExecuteSQL().getName(), updateBlock.getExecuteSQL().getRequiredCheckValue());
                continue;
            }

            int maxCount = 0;
            String maxCountParam = null;
            for (String sqlParameter : sqlParameters) {
                List<Object> values = xmlValues.get(sqlParameter);
                if (values != null && values.size() > maxCount) {
                    maxCount = values.size();
                    maxCountParam = sqlParameter;
                }
            }

            if (maxCount == 0 || maxCountParam == null) {
                logger.error("The received XML might not correspond to the template XML, please check.");
                return false;
            }

            // set Id's if it is an insertSQL or executeSQL and has an idDefinition tag
            if (updateBlock.getInsertSQL() != null && updateBlock.getInsertSQL().getIdDefinition() != null ||
                    updateBlock.getExecuteSQL() != null && updateBlock.getExecuteSQL().getIdDefinition() != null) {

                String idDefinition = updateBlock.getInsertSQL() != null ? updateBlock.getInsertSQL().getIdDefinition() : updateBlock.getExecuteSQL().getIdDefinition();

                Set <String> idSqlParameters = dbuExecutor.getSqlParamsReplacer().getVariablesSet(idDefinition);
                MapSqlParameterSource parameterSource = new MapSqlParameterSource();
                for (String parameter : idSqlParameters) {
                    List<Object> values = xmlValues.get(parameter);
                    if (values != null && !values.isEmpty()) {
                        parameterSource.addValue(parameter, values.get(0));
                    }
                }

                String databaseId = updateBlock.getInsertSQL() != null ? updateBlock.getInsertSQL().getDatabase() : updateBlock.getExecuteSQL().getDatabase();
                DBExecutor selectExecutor = dbuExecutor.getExecutor(executor, databaseId);
                if (selectExecutor != null) {
                    String idParam = null;
                    for (int i = 0; i < maxCount; i++) {
                        List<MapSqlParameterSource> valuesParams = selectExecutor.selectQuery(idDefinition, parameterSource, dbuExecutor.getVariables());
                        for (MapSqlParameterSource source : valuesParams) {
                            for (String name : source.getValues().keySet()) {
                                if (dbuExecutor.getVariables().get(name) == null) {
                                    idParam = name;
                                    List<Object> values = xmlValues.get(name);
                                    if (values == null) {
                                        values = new ArrayList<Object>();
                                        xmlValues.put(name, values);
                                    }
                                    values.add(source.getValue(name));
                                }
                            }
                        }
                    }
                    if (idParam != null) {
                        List<Integer> idValueOrders = new ArrayList<Integer>();
                        idValueOrders.addAll(xmlValuesOrder.get(maxCountParam));
                        xmlValuesOrder.put(idParam, idValueOrders);
                    }
                } else {
                    logger.warn("DB executor {} defined for insert query is invalid in receive task", updateBlock.getInsertSQL().getDatabase());
                }
            }

            List<MapSqlParameterSource> parameterSourceList = new ArrayList<MapSqlParameterSource>(maxCount);
            for (int i = 0; i < maxCount; i++) {
                MapSqlParameterSource parameterSource = new MapSqlParameterSource();

                int minOrderIndex = Integer.MAX_VALUE;
                for (String sqlParameter : sqlParameters) {
                    List<Integer> values = xmlValuesOrder.get(sqlParameter);
                    if (values != null && values.size() == maxCount) {
                        if (minOrderIndex > values.get(i)) {
                            minOrderIndex = values.get(i);
                        }
                    }
                }

                for (String sqlParameter : sqlParameters) {
                    List<Object> values = xmlValues.get(sqlParameter);
                    if (values != null && !values.isEmpty()) {
                        if (values.size() < maxCount) {
                            List<Integer> orderIndexes = xmlValuesOrder.get(sqlParameter);
                            int selectedIndex = 0;
                            if (orderIndexes != null) {
                                for (int index = 0; index < orderIndexes.size(); index++) {
                                    if (orderIndexes.get(index) < minOrderIndex) {
                                        selectedIndex = index;
                                    } else {
                                        break;
                                    }
                                }
                            }
                            parameterSource.addValue(sqlParameter, values.get(selectedIndex));
                        } else {
                            parameterSource.addValue(sqlParameter, values.get(i));
                        }
                    } else {
                        parameterSource.addValue(sqlParameter, null);
                    }
                }
                parameterSourceList.add(parameterSource);
            }
            DBExecutor.executeBatchUpdateBlock(dbuExecutor, updateBlock, executor, parameterSourceList);
        }
        return true;
    }

    protected String getDocumentId(String document, String idTag) {
        logger.debug("Getting value for ID tag: " + idTag);
        return document.substring(document.indexOf("<" + idTag + ">") + idTag.length() + 2, document.indexOf("</" + idTag + ">"));
    }

    protected void sendMQResult(String resultTemplateFile, final Map<String, List<Object>> xmlValues) {
        if (resultTemplateFile != null) {

            Map<String, Object> sqlParams = new HashMap<String, Object>();
            for (String param : dbuExecutor.getSqlVariablesReplacer().getVariablesSet(resultTemplateFile)) {
                List<Object> values = xmlValues.get(PatternReplacer.getVariableSimpleName(param));
                if (values != null && !values.isEmpty()) {
                    sqlParams.put(PatternReplacer.getVariableSimpleName(param), values.get(0));
                }
            }
            StringBuilder resultXml = new StringBuilder(resultTemplateFile);
            if (dbuExecutor.getSqlVariablesReplacer().replaceVariables(resultXml, sqlParams, dbuExecutor.getDateTimeFormats()) > 0) {
                logger.debug("Not all SQL variables could be replaced for result message");
            }
            if (dbuExecutor.getGlobalVariablesReplacer().replaceVariables(resultXml, dbuExecutor.getVariables(), dbuExecutor.getDateTimeFormats()) > 0) {
                logger.debug("Not all Global variables could be replaced for result message");
            }

            final String resultXmlFile = resultXml.toString();
            dbuExecutor.getMessageSender().sendMessage(resultXmlFile);
            logger.debug("Sent Result Message: {}", resultXmlFile);
        }
    }


}
